/**
 * Copyright (c) Daniel Centore
 *
 * ex: set filetype=java expandtab tabstop=4 shiftwidth=4 :
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * This code is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation.
 */
package ronp.anim.gui.viewer;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JViewport;

import ronp.anim.data.Grip;
import ronp.anim.data.filmstrip.Animation;
import ronp.anim.data.filmstrip.Layer;
import ronp.anim.data.objects.RNObject;
import ronp.anim.gui.Gui;
import ronp.anim.gui.KeyInputListener;
import ronp.anim.gui.SelectedState;
import ronp.anim.gui.tools.DraggableTool;
import ronp.anim.gui.tools.ObjectsMenu;
import ronp.anim.gui.tools.Tool;
import ronp.anim.gui.tools.ToolsMenu;

/**
 * Allows us to view the current frame
 * 
 * @author Daniel Centore
 *
 */
public class AnimViewer extends JComponent implements MouseListener, MouseMotionListener, MouseWheelListener
{
	private static final long serialVersionUID = 1L;

	private Animation anim; // The Animation to display
	private SelectedState state; // Currently selected stuff

	private Tool currentTool; // The currently active drawing tool
	private boolean useGrips; // Should we use grips? (meaning draw -and- send events for them)

	private int mouseX; // current mouse X and Y locs
	private int mouseY;
	private int gripSize; // grip diameter in pixels

	private float zoom = 1f; // zoom level (1 = 100% zoom, 2 = 200%, etc)

	private Image underlayer = null; // displayed underneath the image
	private boolean showFade = true;

	// dragging stuff
	private boolean isDragging;
	private Grip startingGrip;
	private int myButton;
	private int xDiff;
	private int yDiff;

	private Point display = new Point(0, 0); // where we should be displaying stuff
	private ObjectsMenu menu = null; // ObjectsMenu we will be displaying (for access once initialized)
	private ToolsMenu tMenu = null;

	private Gui gui;

	/**
	 * Creates an {@link AnimViewer}
	 * @param anim The {@link Animation} to display
	 * @param state The {@link SelectedState} to display of the {@link Animation}
	 */
	public AnimViewer(Animation anim, SelectedState state, Gui gui)
	{
		this.setFocusable(true);
		gripSize = 10;
		this.anim = anim;
		this.state = state;
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.addMouseWheelListener(this);
		useGrips = true;
		this.gui = gui;
	}

	@Override
	public void paintComponent(Graphics g)
	{
		if (menu == null)
			return;

		BufferedImage temp = new BufferedImage(anim.getWidth(), anim.getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics2D g2 = (Graphics2D) temp.getGraphics();

		g2.setColor(Color.white);
		g2.fillRect(0, 0, anim.getWidth(), anim.getHeight());

		if (underlayer != null)
			g2.drawImage(underlayer, 0, 0, anim.getWidth(), anim.getHeight(), null);

		List<RNObject> objects = anim.getObjects();

		if (showFade)
		{
			for (int i = 1; i <= 10; i++)
			{
				float composite = (i == 10 ? 1f : (float) i / 10 / 2);
				g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, composite));
				int frame = state.getActiveFrame() - (10 - i);
				for (RNObject obj : objects)
				{
					obj.drawImage(frame, g2, menu.isSelected(obj));
				}
			}
		}
		else
		{
			for (RNObject obj : objects)
			{
				obj.drawImage(state.getActiveFrame(), g2, menu.isSelected(obj));
			}
		}

		if (currentTool != null)
		{
			Image b = currentTool.getOverlay(mouseX, mouseY);
			g2.drawImage(b, 0, 0, null);
		}

		if (useGrips)
		{
			for (Layer layer : anim.getLayers())
			{
				Grip grip = layer.getGrip();
				if (state.getActiveLayer() != null && state.getActiveLayer().getGrip() == grip)
					g2.setColor(Color.red);
				else
					g2.setColor(Color.blue);

				Point p = grip.getPosition(state.getActiveFrame());
				g2.drawOval(p.x - gripSize / 2, p.y - gripSize / 2, gripSize, gripSize);
			}
		}

		g2.dispose();

		g.drawImage(temp, 0, 0, (int) (anim.getWidth() * zoom), (int) (anim.getHeight() * zoom), null);

		this.setPreferredSize(new Dimension((int) (anim.getWidth() * zoom), (int) (anim.getHeight() * zoom)));
	}

	/**
	 * @return The currently active tool (or null if none active)
	 */
	public Tool getTool()
	{
		return currentTool;
	}

	/**
	 * Sets the active tool
	 * @param tool Tool to set it to (or null for no tool)
	 */
	public void setTool(Tool tool)
	{
		this.currentTool = tool;
	}

	@Override
	public void mousePressed(MouseEvent e)
	{
		this.requestFocusInWindow();

		Point p = scale(e);

		myButton = e.getButton();
		xDiff = p.x;
		yDiff = p.y;

		if (currentTool == null || xDiff > anim.getWidth() || yDiff > anim.getHeight() || xDiff < 0 || yDiff < 0)
			return;

		startingGrip = findGrip(xDiff, yDiff);

		currentTool.mousePressed(p.x, p.y);
	}

	@Override
	public void mouseReleased(MouseEvent e)
	{
		if (isDragging)
		{
			isDragging = false;
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			return;
		}

		if (e.getButton() == 3)
		{
			state.setActiveFrame(state.getActiveFrame() + 1);
			anim.repaint();
			return;
		}

		if (currentTool == null)
			return;

		Point click = scale(e);

		if (click.x > anim.getWidth() || click.y > anim.getHeight() || click.x < 0 || click.y < 0)
			return;

		Layer closest = null;
		double oldDist = Integer.MAX_VALUE;

		for (Layer l : anim.getLayers())
		{
			Point p = l.getGrip().getPosition(state.getActiveFrame());

			double i;
			if ((i = p.distance(click)) < oldDist)
			{
				oldDist = i;
				closest = l;
			}
		}

		if (closest != null && oldDist <= gripSize && useGrips)
			currentTool.gripSelected(closest, click.x, click.y);
		else
			currentTool.mouseReleased(click.x, click.y);
	}

	public Grip findGrip(int x, int y)
	{
		Point click = new Point(x, y);

		Layer closest = null;
		double oldDist = Integer.MAX_VALUE;

		for (Layer l : anim.getLayers())
		{
			Point p = l.getGrip().getPosition(state.getActiveFrame());

			double i;
			if ((i = p.distance(click)) < oldDist)
			{
				oldDist = i;
				closest = l;
			}
		}

		if (closest != null && oldDist <= gripSize && useGrips)
			return closest.getGrip();

		return null;
	}

	@Override
	public void mouseClicked(MouseEvent e)
	{
	}

	@Override
	public void mouseEntered(MouseEvent e)
	{
	}

	@Override
	public void mouseExited(MouseEvent e)
	{
	}

	/**
	 * @return True if using grips is enabled; False otherwise
	 */
	public boolean isUseGrips()
	{
		return useGrips;
	}

	/**
	 * Sets whether or not we are using the grips
	 * @param useGrips True if we are; False otherwise
	 */
	public void setUseGrips(boolean useGrips)
	{
		this.useGrips = useGrips;
	}

	@Override
	public void mouseDragged(MouseEvent e)
	{
		if (myButton == 1)
		{
			if (currentTool instanceof DraggableTool)
			{
				Point p = scale(e);

				if (p.x > anim.getWidth() || p.y > anim.getHeight() || p.x < 0 || p.y < 0)
					return;

				((DraggableTool) currentTool).mouseDragged(p.x, p.y, xDiff, yDiff, startingGrip);
			}
			return;
		}

		setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
		isDragging = true;

		Container c = this.getParent();
		if (c instanceof JViewport)
		{
			JViewport jv = (JViewport) c;
			Point p = jv.getViewPosition();
			int newX = p.x - (e.getX() - xDiff);
			int newY = p.y - (e.getY() - yDiff);

			// == Uncomment below to not allow infinite scrolling == //

			// int maxX = this.getWidth() - jv.getWidth();
			// int maxY = this.getHeight() - jv.getHeight();
			// if (newX < 0)
			// newX = 0;
			// if (newX > maxX)
			// newX = maxX;
			// if (newY < 0)
			// newY = 0;
			// if (newY > maxY)
			// newY = maxY;

			jv.setViewPosition(display = new Point(newX, newY));
		}
	}

	@Override
	public void validate()
	{
		super.validate();

		Container c = AnimViewer.this.getParent();
		if (c instanceof JViewport)
		{
			JViewport jv = (JViewport) c;

			jv.setViewPosition(display);
		}
	}

	@Override
	public void mouseMoved(MouseEvent e)
	{
		Point p = scale(e);
		if (p.y < 0 || p.x > anim.getWidth() || p.y > anim.getHeight() || p.x < 0)
			return;

		mouseX = p.x;
		mouseY = p.y;
		this.repaint();
	}

	/**
	 * @return The current zoom (0 = nothing, 1 = normal size, 2 = double size, etc)
	 */
	public float getZoom()
	{
		return zoom;
	}

	/**
	 * Sets the zoom and repaints
	 * @param zoom The current zoom (0 = nothing, 1 = normal size, 2 = double size, etc)
	 */
	public void setZoom(float zoom)
	{
		if (zoom <= 0)
			throw new RuntimeException("Cannot have zoom <= 0!");

		this.zoom = zoom;
		this.repaint();
		this.validate();
	}

	/**
	 * Scales a mouse position based on the zoom factor
	 * @param e {@link MouseEvent} to grab the coordinates from
	 * @return A {@link Point} of where the mouse would be at 100% zoom
	 */
	private Point scale(MouseEvent e)
	{
		return scale(e.getX(), e.getY());
	}

	/**
	 * Scales a mouse position based on the zoom factor
	 * @param x X and Y coordinates of the mouse
	 * @param y
	 * @return A {@link Point} of where the mouse would be at 100% zoom
	 */
	private Point scale(int x, int y)
	{
		int width = (int) (anim.getWidth() * zoom);
		int height = (int) (anim.getHeight() * zoom);

		int newX = anim.getWidth() * x / width;
		int newY = anim.getHeight() * y / height;

		return new Point(newX, newY);
	}

	/**
	 * @return The size of the grip's diameter in pixels
	 */
	public int getGripSize()
	{
		return gripSize;
	}

	/**
	 * Sets the grips diameter in pixels
	 * @param gripSize Diameter to set it to
	 */
	public void setGripSize(int gripSize)
	{
		this.gripSize = gripSize;
	}

	/**
	 * @return The image we display underneath our {@link Animation} for help
	 */
	public Image getUnderlayer()
	{
		return underlayer;
	}

	/**
	 * Sets the image we display underneath our {@link Animation} for help
	 * @param underlayer The {@link Image} to set it to
	 */
	public void setUnderlayer(Image underlayer)
	{
		this.underlayer = underlayer;
	}

	/**
	 * @return True if we are showing the fade; False otherwise
	 */
	public boolean isShowFade()
	{
		return showFade;
	}

	/**
	 * Sets whether or now we should show the fade
	 * @param showFade True if we should; False otherwise
	 */
	public void setShowFade(boolean showFade)
	{
		this.showFade = showFade;
	}

	/**
	 * @return The {@link ObjectsMenu} in use (or null if not initialized yet)
	 */
	public ObjectsMenu getMenu()
	{
		return menu;
	}

	/**
	 * Sets the {@link ObjectsMenu}. Should only get called once (unchecked).
	 * @param menu The {@link ObjectsMenu} to set it to.
	 */
	public void setMenu(ObjectsMenu menu)
	{
		this.menu = menu;

		KeyInputListener listener = new KeyInputListener(this, anim, state, menu, gui);
		this.addKeyListener(listener);
	}

	@Override
	public void mouseWheelMoved(MouseWheelEvent e)
	{
		int clicks = e.getWheelRotation();
		boolean out = (clicks < 0);
		clicks = Math.abs(clicks);

		try
		{
			if (out)
			{
				for (int i = 0; i < clicks; i++)
					this.setZoom(zoom + 0.05f);
			}
			else
			{
				for (int i = 0; i < clicks; i++)
					this.setZoom(zoom - 0.05f);
			}
		} catch (Exception e1)
		{
		}

		if (tMenu != null)
			tMenu.getZoomSpin().setValue((int) (zoom * 100));
	}

	public ToolsMenu gettMenu()
	{
		return tMenu;
	}

	public void settMenu(ToolsMenu tMenu)
	{
		this.tMenu = tMenu;
	}
}
